'''
featherinyourcap

A game by Karl Urdevics and Jervis Whitley.

Released under the MIT license (see README.txt for details, tnx.)

havfunbye.
'''
import sys
import data
import pygame
pygame.mixer.pre_init(44100, -16, 2, 1024*3)
from pygame.locals import *
from random import randint, choice
import random
import math

import constants as c
import fan
import air
import bird
import scorebar
import animation as anim
import levels
import turbine
import turbinepole
import menu
import popup
import sounds
import thebonus

def main():
    pygame.init()
    sounds.play_bgmusic('enemy.xm')

    # set up a hello world screen and background
    display = pygame.display.set_mode(c.RESOLUTION)

    pygame.display.set_caption('featherinyourcap')
    pygame.mouse.set_visible(1)

    # a list of levels located in the levels module.
    level_list = [(l.name, l) for l in levels.levels]

    menubg, _= data.load_image("menu_bg.png", None)
    # show the menu screen.
    while True:
        # reset game stats if someone presses quit!
        gamestats = GameStats()
        try:
            menu.main(display, menubg, level_list)
        except menu.CommsException:
           
            # user has selected something.
            msg = get_msg_from_last_exception()
            
            try:
                index = list(zip(*level_list)[0]).index(msg)
            except ValueError: # user may have entered 'quit' - not a level.
                break

        completed = []
        for name, level in level_list[index:]:
            running = True
            attempts = 0
            while running:
                try:
                    output, stats = runlevel(level, display).next()
                    attempts += 1
                    try:
                        stats.attempts = attempts
                    except AttributeError:
                        pass
                except StopIteration:
                    # user really wants to quit!!
                    return
                # now check the type of bloody output.
                if output == 'success':
                    bonuses = gamestats.update(stats, True)
                    stats.bonus = bonuses
                    popup.success_or_fail(display, True, stats)

                    if not name in completed:
                        completed.append(name)
                    break
                elif output == 'quit':
                    # some kind of 'are u sure' crap here.
                    running = False
                elif output == 'failure':
                    bonuses = gamestats.update(stats, False)
                    stats.bonus = bonuses
                    popup.success_or_fail(display, False, stats)
                    # retry the level sonny!!
                    continue
                else:
                    raise Exception("WHAT is %s" % output)
            if not running:
                break

        if running and len(completed) == len(level_list):
            # user completed the game in one sitting!
            #sounds.play_bgmusic('ateam.ogg')
            popup.game_completed(display)


def runlevel(config, display):
    """run each of the levels.
    config should define.

    * number of birds. dictionary of birdtypes and numbers
    * (x, y) of turbines in a list.
    * number to die for failure.
    
    game is essentially...
    put turbines in their place.
    make bird of given type until no more required.
    track birds that have died.
    exit when too many birds died.
    """

    background, background_rect = data.load_image("bg.png", None)
   
    # set up our status text message
    if pygame.font:
        # some monkey patching for exe
        if 'python' not in sys.executable and not hasattr(pygame.font, '_Font'):
            pygame.font._Font = pygame.font.Font
            def f(item, size):
                pth = data.filepath('Vera.ttf')
                return pygame.font._Font(item or pth, size-10)
            pygame.font.Font = f
        font = pygame.font.Font(None, 36)
    
        
    # the initial filling default black to the background.
    pygame.display.flip()

    # a clock to limit our FPS.
    clk = pygame.time.Clock()

    # generic sprite group.
    fans = pygame.sprite.Group()
    # bird group.
    birds = pygame.sprite.Group()
    # air group
    airs = pygame.sprite.Group()
    # bird group.
    birds = pygame.sprite.Group()
    # turbine group
    turbines = pygame.sprite.Group()
    # turbinepoles group
    turbinepoles = pygame.sprite.Group()

    # a default screen sprite for collision (i.e is sprite on screen.)
    screen_sprite = pygame.sprite.Sprite()
    screen_sprite.rect = display.get_rect()
    # the top bar of the screen for showing score etc..
    scores = pygame.sprite.Group()
    showscore = pygame.sprite.Group()
    scoresprite = scorebar.Scorebar()
    # a hidden bottom bar for stopping birds too low.
    _bottombar = scorebar.Scorebar()
    _bottombar.rect.topleft = (0, c.YMAX - 1)
    scores.add(scoresprite)
    showscore.add(scoresprite)
    scores.add(_bottombar)
    # for animations.
    animations = pygame.sprite.Group()

    # created a fan class and added it to the sprite group.
    myfan = fan.Fan()
    fans.add(myfan)

    # a local copy of the birddict from config.
    levelbirds = config.birds.copy()

    def make_turbines(group, turbine_coords):
        for x, y in turbine_coords:
            group.add(turbine.Turbine(x, y))

    make_turbines(turbines, config.turbines)

    # create turbinepole sprites
    for t in turbines:
        turbinepoles.add(turbinepole.Turbinepole(t.rect.centerx,
                    t.rect.centery))

    # create a timer to set off events for birds to update.
    pygame.time.set_timer(c.BIRDUPDATE, c.BIRDRATE)

    # game data.
    saved = 0
    dead = 0
    kWh = 0
    total_time = 0
    # targetkWh calculate theoretical maximum first.
    maxkWh = config.timelimit * c.TURBINE_kW * len(turbines) / 3600.0
    target_kWh = round(maxkWh * config.target_percent / 100.0, 1)
    config.target_kWh = target_kWh
    
    # create a popup to show the user the level instructions.
    popup.popup(display, config)
    
    time_last = pygame.time.get_ticks()
    running = True
    # main game loop.
    while running:


        # slow the frame rate to FPS - FPS too high = bad results
        clk.tick(c.FPS)

        # update teh time difference
        time_current = pygame.time.get_ticks()
        time_diff = time_current - time_last
        time_last = time_current
        total_time += time_diff
        
        # dirtily erase the entire screen, only to reblit things later.
        display.blit(background, (0, 0))


        # handle events
        # this is the NUTS AND GUTS of the game
        airs_made = 0
        for event in pygame.event.get():

            if event.type == QUIT:
                running = False
                break

            if event.type == KEYDOWN:
                if event.key == K_q:
                    yield 'quit', None

            if event.type == MOUSEBUTTONDOWN:
                for f in fans:
                    fan_ready = f.click(event.button)
                    if event.button == 1:
                        airs.add(air.Air(c.BLOW, 0.0, 400.0, 10.0))

                    elif event.button == 3:
                        airs.add(air.Air(c.SUCK, 0.0, 400.0, 10.0))
                    airs_made += 1

            if event.type == c.BIRDUPDATE:
                birds.update({})

            if event.type == c.EVENT_FAN_TIMEOUT:
                myfan.timeout()

        # handle updating sprites.

        # step 1. update the fan with mouse position.
        fans.update(time_diff)
        birds.update({'time': time_diff})
        airs.update(time_diff)
        scores.update({'dead birds': dead,
                        'saved birds': saved,
                        'time elapsed': str(round(total_time/1000.0, 1)),
                        'kWh generated': str(round(kWh, 1))})
        animations.update({'time': time_diff})
        turbines.update(time_diff)

        # make a random bird up to the limit specified in the config.
        try:
            # this next random.choice raises IndexError when no birds left.
            birdname = random.choice(levelbirds.keys())

            # how many birds on target.
            badbirds = ontarget(birds, turbines)
            if badbirds > config.max_ontarget:
                raise IndexError("Too many birds on target, no more!")
        except IndexError:
            # no more birds left to make!!
            pass
        else:
            BirdType, birdchance = levelbirds[birdname]
            y = make_bird(birds, animations, BirdType, birdchance)
            if y:
                # check the if the y is in line with a turbine.
                # and credit with a bonus to allow a single shot to save bird.
                kWh += sum(t.rect.top < y and t.rect.bottom > y
                                          for t in turbines) * c.BIRD_BONUS

        # remove sprites that are no longer on the screen.
        saved += remove_offscreen_sprites(screen_sprite, birds)
        remove_offscreen_sprites(screen_sprite, airs)
        bounce_topscreen_sprites(scores, birds)

        dead += handle_collision(birds, turbines, perpixel=True)

        # update the kWh this will be replaced with something more robust soon.
        for t in turbines:
            kWh += time_diff / 1000.0 / 3600 * t.output

        # subtract an amount for the airs created.
        kWh -= airs_made * c.AIRS_COST

        class Gameobj:pass
        'if we need to expand this stats concept this class will need to move.'
        g = Gameobj()
        g.target_kWh = target_kWh
        g.total_kWh = round(kWh, 1)
        g.saved = saved
        g.dead = dead
        g.bonus = []

        if (round(kWh, 1) >= round(target_kWh)
                and total_time / 1000.0 > config.timelimit):
            yield 'success', g

        # failure is when no time remaining already checked the kWh.
        if total_time / 1000.0 > config.timelimit:
            yield 'failure', g


        handle_collision(birds, airs)

        # draw fps to display.
        #handle_fps(time_diff, scoresprite)

        # dirty way to draw everything directly to the main display.
        for group in [turbinepoles, fans, airs, birds,
                            showscore, animations, turbines]:
            group.draw(display)

        # now show.
        pygame.display.flip()

def handle_collision(birds, other, perpixel=None):
    'handle collisions between birds and some other group.'
    hitmap = pygame.sprite.groupcollide(birds, other, False, False)
    dead = []
    for bird, others in hitmap.items():
        if perpixel:
            # now find the things that bird hit on a perpixel basis.
            collide_mask = pygame.sprite.collide_mask
            hit = pygame.sprite.spritecollide(bird, others, False, collide_mask)
        else:
            hit = others

        for o in hit:
            dead.append(o)
            bird.collide(o)
            o.collide(bird)
    return len(dead)

def bounce_topscreen_sprites(other, group):
    'sprites have elastic collision with other (assumed to be walls).'
    hitmap = pygame.sprite.groupcollide(other, group, False, False)
    for wall, birds in hitmap.items():
        for bird in birds:
            # perfect elastic collision with infinite mass object (wall).
            bird.vely = -bird.vely

def remove_offscreen_sprites(screen, group):
    onscreen = pygame.sprite.spritecollide(screen, group, dokill=False)
    # find those that are not in the onscreen group.
    offscreen = set(onscreen) ^ set(group)
    group.remove(offscreen)
    return len(offscreen)

def make_bird(birds, animgroup, BirdType, birdchance):
    'a random chance to make a bird and place in the birds group.'
    if not randint(0, birdchance):
        # make a bird with 1 in 101 chance.
        x = c.XMAX - 100
        y = randint(100, c.YMAX / 2)
        birds.add(BirdType(x, y, animgroup))
        return y
    return None

def ontarget(birds, turbines):
    'how many birds are on target to hit turbine .. roughly..'
    willcollide = 0
    for b in birds:
        willcollide += sum(t.rect.top < b.y and t.rect.bottom > b.y
                                for t in turbines)
    return willcollide

class GameStats(object):
    'just a holder for all our end game stats. used as singleton'
    total_kWh = 0
    bonuses = []
    dead = 0
    saved = 0
    attempts = 0

    def update(self, stats, success):
        'update our internal stats with some new ones.'
        if success:
            self.total_kWh += getattr(stats, 'total_kWh', 0)
            self.bonuses += getattr(stats, 'bonus', [])
            self.dead += getattr(stats, 'dead', 0)
            self.saved += getattr(stats, 'saved', 0)
        self.attempts += getattr(stats, 'attempts', 1)

        # did the user get a bonus?
        return thebonus.get_bonuses(self, stats)

def get_msg_from_last_exception():
    return sys.exc_info()[1].args[0]


